﻿using System;
using System.Collections.Generic;
using System.Linq;
using UnityEngine;

namespace PerfAssist.MemoryProfilerAdvanced.Editor.Treemap
{
    public class Utility
    {
        public static Rect[] GetTreemapRects(float[] values, Rect targetRect)
        {
            if (values.Length == 0)
                throw new ArgumentException("You need to at least pass in one valid value", "values");

            var result = new Rect[values.Length];

            var totalInputArea = 0f;
            for (var i = 0; i < values.Length; i++)
                totalInputArea += values[i];

            var totalOutputArea = targetRect.width * targetRect.height;
            var vertical = targetRect.width > targetRect.height;

            var unfinishedRects = new List<Rect>();

            for (var index = 0; index < values.Length; index++)
            {
                var lastItem = index == values.Length - 1;

                var currentInputValue = values[index];

                if (currentInputValue <= 0f)
                    throw new ArgumentException("only positive float values are supported. found: " +
                                                currentInputValue);

                var currentOutputArea = currentInputValue * totalOutputArea / totalInputArea;
                unfinishedRects = AddRect(unfinishedRects, currentOutputArea, targetRect, vertical);
                var currentAspect = GetAverageAspect(unfinishedRects);

                var nextInputValue = lastItem ? 0f : values[index + 1];
                var nextOutputArea = nextInputValue * totalOutputArea / totalInputArea;
                var nextAspect = GetNextAspect(unfinishedRects, nextOutputArea, targetRect, vertical);

                if (Mathf.Abs(1f - currentAspect) < Mathf.Abs(1f - nextAspect) || lastItem)
                {
                    var resultIndex = index - unfinishedRects.Count + 1;
                    for (var rectIndex = 0; rectIndex < unfinishedRects.Count; rectIndex++)
                        result[resultIndex++] = unfinishedRects[rectIndex];

                    targetRect = GetNewTarget(unfinishedRects, targetRect, vertical);
                    vertical = !vertical;
                    unfinishedRects.Clear();
                }
            }

            return result;
        }

        private static List<Rect> AddRect(List<Rect> existing, float area, Rect space, bool vertical)
        {
            var result = new List<Rect>();
            if (vertical)
            {
                if (existing.Count == 0)
                {
                    result.Add(new Rect(space.xMin, space.yMin, area / space.height, space.height));
                }
                else
                {
                    var totalSize = GetArea(existing) + area;
                    var width = totalSize / space.height;
                    var yPosition = space.yMin;
                    foreach (var old in existing)
                    {
                        var itemArea = GetArea(old);
                        result.Add(new Rect(old.xMin, yPosition, width, itemArea / width));
                        yPosition += itemArea / width;
                    }

                    result.Add(new Rect(space.xMin, yPosition, width, area / width));
                }
            }
            else
            {
                if (existing.Count == 0)
                {
                    result.Add(new Rect(space.xMin, space.yMin, space.width, area / space.width));
                }
                else
                {
                    var totalSize = GetArea(existing) + area;
                    var height = totalSize / space.width;
                    var xPosition = space.xMin;
                    foreach (var old in existing)
                    {
                        var itemArea = GetArea(old);
                        result.Add(new Rect(xPosition, old.yMin, itemArea / height, height));
                        xPosition += itemArea / height;
                    }

                    result.Add(new Rect(xPosition, space.yMin, area / height, height));
                }
            }

            return result;
        }

        private static Rect GetNewTarget(List<Rect> unfinished, Rect oldTarget, bool vertical)
        {
            if (vertical)
                return new Rect(oldTarget.xMin + unfinished[0].width, oldTarget.yMin,
                    oldTarget.width - unfinished[0].width, oldTarget.height);
            return new Rect(oldTarget.xMin, oldTarget.yMin + unfinished[0].height, oldTarget.width,
                oldTarget.height - unfinished[0].height);
        }

        private static float GetNextAspect(List<Rect> existing, float area, Rect space, bool vertical)
        {
            var newExisting = AddRect(existing, area, space, vertical);
            return newExisting[newExisting.Count - 1].height / newExisting[newExisting.Count - 1].width;
        }

        private static float GetAverageAspect(List<Rect> rects)
        {
            var aspect = 0f;
            foreach (var r in rects) aspect += r.height / r.width;
            return aspect / rects.Count;
        }

        private static float GetArea(Rect rect)
        {
            return rect.width * rect.height;
        }

        private static float GetArea(List<Rect> rects)
        {
            return rects.Sum(x => GetArea(x));
        }

        public static Color GetColorForName(string name)
        {
            int r = 0, g = 0, b = 0;

            for (var i = 0; i < name.Length; i++)
                if (i % 3 == 0)
                    r += name[i];
                else if (i % 3 == 1)
                    g += name[i];
                else
                    b += name[i];

            r %= 128;
            g %= 128;
            b %= 128;

            return new Color32((byte) (r + 96), (byte) (g + 96), (byte) (b + 96), 255);
        }

        public static bool IsInside(Rect lhs, Rect rhs)
        {
            return lhs.xMax > rhs.xMin && lhs.xMin < rhs.xMax && lhs.yMax > rhs.yMin && lhs.yMin < rhs.yMax;
        }
    }
}